/*
 * Copyright (C) 2012  Forklabs Daniel Léonard
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package ca.forklabs.wow.net;

import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.ResourceBundle;
import ca.forklabs.baselib.io.StreamUtil;
import ca.forklabs.baselib.util.Algorithm;
import ca.forklabs.baselib.util.CharsetPropertiesControl;
import ca.forklabs.baselib.util.Iterators;
import ca.forklabs.baselib.util.StringUtil;
import ca.forklabs.baselib.util.UnaryFunction;
import ca.forklabs.baselib.util.UnaryPredicate;
import ca.forklabs.wow.net.realms.RealmList;
import ca.forklabs.wow.net.ssl.certificates.ImportCertificatesFromBattlenetComCn;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;

/**
 * Class {@code WoWAPI} provides useful methods to access the World of Warcraft
 * API.
 * <p>
 * This API wraps around a {@link BNetConnection} to access <em>Battle.net</em>
 * and calls its non-secure methods. To be able to make secure calls, the usage
 * of {@link AuthenticatedBNetConnection} or your own implementation is
 * suggested.
 * <p>
 * This class also holds two default values: one for the <em>last modified</em>
 * timestamp and another for the language. The default value for the timestamp
 * is {@link #WORLD_OF_WARCRAFT_BEGINS} and the default language is
 * {@link LanguageFields#en_US}.
 * <p>
 * Finally, this class holds an instance of a realm list. This instance allow
 * any queries having a reference to a realm name to use its slug instead.
 *
 * @author   <a href="mailto:forklabs at gmail.com?subject=ca.forklabs.wow.net.WoWAPI">Daniel Léonard</a>
 * @version $Revision$
 */
public class WoWAPI {

//---------------------------
// Enums
//---------------------------

   /**
    * Lists all the possible fields for a character call.
    * @see   <a href="http://blizzard.github.com/api-wow-docs/#id3380301">Blizzard Community Platform API Documentation</a>
    */
   public static enum CharacterFields {
      /** A summary of the guild that the character belongs to. */
      Guild,
      /** A map of character attributes and stats. */
      Stats,
      /**A list of talent structures.  */
      Talents,
      /** A list of items equipted by the character. */
      Items,
      /** A list of the factions that the character has an associated reputation with. */
      Reputation,
      /** A list of the titles obtained by the character including the currently selected title. */
      Titles,
      /** A list of the character's professions. */
      Professions,
      /** A map of values that describes the face, features and helm/cloak display preferences and attributes. */
      Appearance,
      /** A list of all of the non-combat pets obtained by the character. */
      Companions,
      /** A list of all of the mounts obtained by the character. */
      Mounts,
      /** A list of all of the combat pets obtained by the character. */
      Pets,
      /** A map of achievement data including completion timestamps and criteria information. */
      Achievements,
      /** A list of raids and bosses indicating raid progression and completedness. */
      Progression,
      /** A map of pvp information including arena team membership and rated battlegrounds information. */
      Pvp,
      /** A list of quests completed by the character. */
      Quests,
      }

   /**
    * Lists all the possible
    * Class {@code LanguageFields} does something.
    *
    * @author   <a href="mailto:forklabs at gmail.com?subject=ca.forklabs.wow.net.LanguageFields">Daniel Léonard</a>
    * @version $Revision$
    */
   public enum LanguageFields {
      /** American English, works everywhere. */
      en_US,
      /** Mexican Spanish, works on the US realms. */
      es_MX,
      /** Brazilian Portuguese, works on the US realms. */
      pt_BR,
      /** British English, works on the European realms. */
      en_GB,
      /** Castilian Spanish, works on the European realms. */
      es_ES,
      /** French, works on the European realms. */
      fr_FR,
      /** Russian, works on the European realms. */
      ru_RU,
      /** German, works on the European realms. */
      de_DE,
      /** Portuguese, works on the European realms. */
      pt_PT,
      /** Italian, works on the European realms. */
      it_IT,
      /** Korean, works on the Korean realms. */
      ko_KR,
      /** Traditional Chinese, works on the Taiwanese realms. */
      zh_TW,
      /** Simplified Chinese, works on the Chinese realms. */
      zh_CN,
      }


//---------------------------
// Inner classes
//---------------------------

   /**
    * Class {@code Answer} is the returned value from all the query methods. It
    * contains the HTTP return code, the JSON text and the URL called.
    * @see   HttpURLConnection
    */
   public static class Answer {
      /** The HTTP code. */
      public int code = 500;
      /** The JSON text. */
      public String json = "{\"status\":\"nok\", \"reason\": \"Something bad happened in the code.\"}"; //$NON-NLS-1$
      /** The URL. */
      public URL url;
      }


//---------------------------
// Class variables
//---------------------------

   /** A good approximation of the launch date of <em>World of Warcraft</em>. */
   public static final long WORLD_OF_WARCRAFT_BEGINS = 1101168000000L; // November 23rd 2004


//---------------------------
// Instance variables
//---------------------------

   /** The connection to <em>Battle.net</em>. */
   private BNetConnection connection;

   /** The default last modified value for this instance of the API. */
   private long last_modified;
   /** The default value for the language of the query. */
   private LanguageFields language;

   /** The list of realms. */
   private RealmList realms;


//---------------------------
// Constructor
//---------------------------

   /**
    * Constructs a new {@code WoWAPI} object with a default
    * {@link BNetConnection} and {@link RealmList}.
    * @throws   IOException   if anything goes wrong loading the realm list.
    */
   public WoWAPI() throws IOException {
      this(new BNetConnection(), new RealmList());
      }

   /**
    * Insider constructor.
    * @param   connection   the connection.
    * @param   realms   the realm list.
    */
   protected WoWAPI(BNetConnection connection, RealmList realms) {
      this(connection, realms, WORLD_OF_WARCRAFT_BEGINS, LanguageFields.en_US);
      }

   /**
    * Constructor.
    * @param   connection   the connection.
    * @param   realms   the realm list.
    * @param   last_modified   the last modified default value.
    * @param   language   the default query language.
    */
   public WoWAPI(BNetConnection connection, RealmList realms, long last_modified, LanguageFields language) {
      this.setBNetConnection(connection);
      this.setRealmList(realms);
      this.setLastModified(last_modified);
      this.setLanguage(language);
      }


//---------------------------
// Accessors and mutators
//---------------------------

   /**
    * Changes the connection.
    * @param   connection   the new connection.
    */
   public void setBNetConnection(BNetConnection connection) {
      this.connection = connection;
      }

   /**
    * Gets the connection.
    * @return   the connection.
    */
   public BNetConnection getBNetConnection() {
      return this.connection;
      }

   /**
    * Changes the default last modified value.
    * @param   last_modified   the new last modified.
    */
   public void setLastModified(long last_modified) {
      this.last_modified = last_modified;
      }

   /**
    * Gets the last modified value.
    * @return   the last modified value.
    */
   public long getLastModified() {
      return this.last_modified;
      }

   /**
    * Changes the default language used for querying.
    * @param   language   the new language.
    */
   public void setLanguage(LanguageFields language) {
      this.language = language;
      }

   /**
    * Gets the default language used for querying.
    * @return   the language.
    */
   public LanguageFields getLanguage() {
      return this.language;
      }

   /**
    * Changes the default realm list.
    * @param   realms   the new realm list.
    */
   public void setRealmList(RealmList realms) {
      this.realms = realms;
      }

   /**
    * Gets the default realm list.
    * @return   the realm list.
    */
   public RealmList getRealmList() {
      return this.realms;
      }


//---------------------------
// Instance methods
//---------------------------

   /**
    * Gets the slug for the given realm name. If the slug does not exist (or
    * the name happens to be a slug itself), its value is set to the given name.
    * @param   region   the region.
    * @param   name   the realm name.
    * @return   the realm slug.
    */
   @SuppressWarnings("hiding")
   protected String getSlugFor(Region region, String name) {
      RealmList realms = this.getRealmList();
      String slug = realms.getSlugFor(region, name);
      if (null == slug) {
         slug = name;
         }
      return slug;
      }

   /**
    * Assemble the queries into a single query string.
    * @param   queries   the queries.
    * @return    the query string.
    */
   public String assembleQueryString(String... queries) {
      String query_string = ""; //$NON-NLS-1$

   // remove empty queries
   // Arrays.asList() is not mutable, we need a mutable list
      List<String> list = new LinkedList<String>(Arrays.asList(queries));
      Algorithm.removeIf(list, new UnaryPredicate<String>() {
         public boolean eval(String query) {
            boolean remove = StringUtil.isBlank(query);
            return remove;
            }
         });
      queries = list.toArray(new String[0]);

      if (0 != queries.length) {
         StringBuilder sb = new StringBuilder();
         sb.append("?").append(StringUtil.join("&", (Object[]) queries)); //$NON-NLS-1$ //$NON-NLS-2$
         query_string = sb.toString();
         }

      return query_string;
      }

   /**
    * Prepares one query.
    * @param   name   the name of the query.
    * @param   values   the values of the query string.
    * @return   the query.
    */
   public String prepareOneQuery(String name, String... values) {
      String query = ""; //$NON-NLS-1$

      if (0 != values.length) {
         query = new StringBuilder()
                  .append(name).append("=").append(StringUtil.join(",", (Object[]) values)) //$NON-NLS-1$ //$NON-NLS-2$
                  .toString();
         }

      return query;
      }

   /**
    * Prepare a language query.
    * @param   language   the language.
    * @return   the query.
    */
   @SuppressWarnings("hiding")
   public String prepareLanguageQuery(LanguageFields language) {
      String base = "locale"; //$NON-NLS-1$
      String fields = language.name();
      String query = this.prepareOneQuery(base, fields);
      return query;
      }

   /**
    * Prepares a query specific to character requests.
    * @param   fields   the fields.
    * @return   the query.
    */
   public String prepareCharacterQuery(CharacterFields... fields) {
      String base = "fields"; //$NON-NLS-1$

      int len = fields.length;
      String[] values = new String[len];

      Algorithm.transform(Iterators.asIterator(fields), Iterators.asIterator(values), new UnaryFunction<String, CharacterFields>() {
         public String invoke(CharacterFields field) {
            String value = field.name().toLowerCase();
            return value;
            }
         });

      String query = this.prepareOneQuery(base, values);
      return query;
      }

   /**
    * Prepares a query specific to realm status requests.
    * @param   region   the region.
    * @param   names   the realm names.
    * @return   the query.
    */
   public String prepareRealmQuery(Region region, String... names) {
      String base = "realms"; //$NON-NLS-1$

      int len = names.length;
      String[] slugs = new String[len];

      final Region the_region = region;
      Algorithm.transform(Iterators.asIterator(names), Iterators.asIterator(slugs), new UnaryFunction<String, String>() {
         public String invoke(String name) {
            String slug = WoWAPI.this.getSlugFor(the_region, name);
            return slug;
            }
         });

      String query = this.prepareOneQuery(base, slugs);
      return query;
      }

   /**
    * Makes the name url-friendly and signature-friendly.
    * @param   name   the name.
    * @return   a friendly name.
    */
   protected String prepareName(String name) {
      String friendlier = name;
      try {
         friendlier = URLEncoder.encode(friendlier, "UTF8"); //$NON-NLS-1$
         }
      catch (UnsupportedEncodingException uee) {
      // nothing
         }
      return friendlier;
      }

   /**
    * Makes the realm url-friendly and signature-friendly.
    * @param   realm   the realm.
    * @return   a friendly realm.
    */
   protected String prepareRealm(String realm) {
      String friendlier = realm;
      try {
      // all spaces must be converted to %20
         friendlier = friendlier.replaceAll(" ", "%20"); //$NON-NLS-1$ //$NON-NLS-2$
      // then everything must be url encoded
         friendlier = URLEncoder.encode(friendlier, "UTF8"); //$NON-NLS-1$
      // but ', ( and ) have to be there, undo
      // http://us.battle.net/wow/en/forum/topic/3963916885
         friendlier = friendlier.replaceAll("%27", "'"); //$NON-NLS-1$ //$NON-NLS-2$
         friendlier = friendlier.replaceAll("%28", "("); //$NON-NLS-1$ //$NON-NLS-2$
         friendlier = friendlier.replaceAll("%29", ")"); //$NON-NLS-1$ //$NON-NLS-2$
      // and the url-encoding of %20 as %2520 must be undone
         friendlier = friendlier.replaceAll("%25", "%"); //$NON-NLS-1$ //$NON-NLS-2$
         }
      catch (UnsupportedEncodingException uee) {
      // nothing
         }
      return friendlier;
      }

   /**
    * Connects to <em>Battle.net</em> and prepares the answer. This method is
    * used mostly by all the other getXXXX() methods as it assumes the resource
    * string to be correct for signature if necessary.
    * @param   region   the region.
    * @param   resource   the resource.
    * @return   an answer with the HTTP code and JSON text.
    * @throws   IOException   if anything goes wrong with I/O.
    */
   @SuppressWarnings("hiding")
   public Answer call(Region region, String resource) throws IOException {
      long last_modified = this.getLastModified();
      Answer answer = this.call(region, resource, last_modified);
      return answer;
      }

   /**
    * Connects to <em>Battle.net</em> and prepares the answer. This method is
    * used mostly by all the other getXXXX() methods as it assumes the resource
    * string to be correct for signature if necessary.
    * @param   region   the region.
    * @param   resource   the resource.
    * @param   last_modified   the last modified token, it overrides {@link #getLastModified()}.
    * @return   an answer with the HTTP code and JSON text.
    * @throws   IOException   if anything goes wrong with I/O.
    */
   @SuppressWarnings({ "hiding", "static-access" })
   public Answer call(Region region, String resource, long last_modified) throws IOException {
   // install the certificates for China only if necessary
      if (Region.China == region) {
         ImportCertificatesFromBattlenetComCn.importCertificates();
         }

      Answer answer = new Answer();

      HttpURLConnection connection = null;
      try {
         BNetConnection bnet_connection = this.getBNetConnection();
         connection = bnet_connection.openConnection(region, resource, last_modified);
         connection.connect();

         answer.url = connection.getURL();
         answer.code = connection.getResponseCode();

         InputStream is = connection.getInputStream();
         Charset charset = StreamUtil.UTF_8;
         answer.json = StreamUtil.readAllText(is, charset);
         }
      catch (FileNotFoundException fnfe) {
      // when using a secure connection and getting a 404 not found,
      // we get this exception instead of the text, go figure
         URL url = (null != connection) ? connection.getURL() : null;
         answer.json = (null != url) ? this.getFileNotFoundErrorMessage(url) : this.getResourceNotFoundErrorMessage();
         }
      finally {
         if (null != connection) {
            connection.disconnect();
            }
         }

      return answer;
      }

   /**
    * Gets the character information.
    * @param   region   the region.
    * @param   realm   the realm.
    * @param   name   the name.
    * @param   fields   the desired fields.
    * @return   an answer with the HTTP code and JSON text.
    * @throws   IOException   if anything goes wrong with I/O.
    * @see   <a href="http://blizzard.github.com/api-wow-docs/#id3380301">http://blizzard.github.com/api-wow-docs/#id3380301</a>
    */
   @SuppressWarnings("hiding")
   public Answer getCharacter(Region region, String realm, String name, CharacterFields... fields) throws IOException {
      long last_modified = this.getLastModified();
      Answer answer = this.getCharacter(region, realm, name, last_modified, fields);
      return answer;
      }

   /**
    * Gets the character information.
    * @param   region   the region.
    * @param   realm   the realm.
    * @param   name   the name.
    * @param   last_modified   the last modified token.
    * @param   fields   the desired fields.
    * @return   an answer with the HTTP code and JSON text.
    * @throws   IOException   if anything goes wrong with I/O.
    * @see   <a href="http://blizzard.github.com/api-wow-docs/#id3380301">http://blizzard.github.com/api-wow-docs/#id3380301</a>
    */
   @SuppressWarnings("hiding")
   public Answer getCharacter(Region region, String realm, String name, long last_modified, CharacterFields... fields) throws IOException {
      LanguageFields language = this.getLanguage();
      Answer answer = this.getCharacter(region, realm, name, last_modified, language, fields);
      return answer;
      }

   /**
    * Gets the character information.
    * @param   region   the region.
    * @param   realm   the realm.
    * @param   name   the name.
    * @param   last_modified   the last modified token.
    * @param   language   the language
    * @param   fields   the desired fields.
    * @return   an answer with the HTTP code and JSON text.
    * @throws   IOException   if anything goes wrong with I/O.
    * @see   <a href="http://blizzard.github.com/api-wow-docs/#id3380301">http://blizzard.github.com/api-wow-docs/#id3380301</a>
    */
   @SuppressWarnings("hiding")
   public Answer getCharacter(Region region, String realm, String name, long last_modified, LanguageFields language, CharacterFields... fields) throws IOException {
      String slug = this.getSlugFor(region, realm);
      slug = this.prepareRealm(slug);

      name = this.prepareName(name);

      String language_query = this.prepareLanguageQuery(language);
      String character_query = this.prepareCharacterQuery(fields);
      String query = this.assembleQueryString(language_query, character_query);

      String pattern = "/api/wow/character/{0}/{1}{2}"; //$NON-NLS-1$
      String resource = MessageFormat.format(pattern, slug, name, query);

      Answer answer = this.call(region, resource, last_modified);
      return answer;
      }

   /**
    * Gets the realm status.
    * @param   region   the region.
    * @param   realms   the desired realms, keep empty for all realms.
    * @return   an answer with the HTTP code and JSON text.
    * @throws   IOException   if anything goes wrong with I/O.
    * @see   <a href="http://blizzard.github.com/api-wow-docs/#id3381924">http://blizzard.github.com/api-wow-docs/#id3381924</a>
    */
   @SuppressWarnings("hiding")
   public Answer getRealmStatus(Region region, String... realms) throws IOException {
      LanguageFields language = this.getLanguage();
      Answer answer = this.getRealmStatus(region, language, realms);
      return answer;
      }

   /**
    * Gets the realm status.
    * @param   region   the region.
    * @param   language   the language.
    * @param   realms   the desired realms, keep empty for all realms.
    * @return   an answer with the HTTP code and JSON text.
    * @throws   IOException   if anything goes wrong with I/O.
    * @see   <a href="http://blizzard.github.com/api-wow-docs/#id3381924">http://blizzard.github.com/api-wow-docs/#id3381924</a>
    */
   @SuppressWarnings("hiding")
   public Answer getRealmStatus(Region region, LanguageFields language, String... realms) throws IOException {
      String language_query = this.prepareLanguageQuery(language);
      String realm_query = this.prepareRealmQuery(region, realms);
      String query = this.assembleQueryString(language_query, realm_query);

      String pattern = "/api/wow/realm/status{0}"; //$NON-NLS-1$
      String resource = MessageFormat.format(pattern, query);

      Answer answer = this.call(region, resource);
      return answer;
      }


//---------------------------
// Localization
//---------------------------

   /** Key for the mock JSON message when a resource not found happens. */
   @SuppressWarnings("nls")
   private static final String RESOURCE_NOT_FOUND = "resource.not.found";
   /** Key for the mock JSON message when a file not found happens in call(). */
   @SuppressWarnings("nls")
   private static final String FILE_NOT_FOUND = "file.not.found";

   /**
    * Gets a generic resource not found JSON message.
    * @return   the error message in JSON format.
    */
   protected String getResourceNotFoundErrorMessage() {
      String message = getLocalizedString(RESOURCE_NOT_FOUND);
      return message;
      }

   /**
    * Gets the message to use in the JSON string when a file not found occurs.
    * @param   url   the faulty URL.
    * @return   the error message in JSON format.
    */
   protected String getFileNotFoundErrorMessage(URL url) {
      String message = getLocalizedString(FILE_NOT_FOUND, url);
      return message;
      }

   /**
    * Gets and formats the specified localized string from the menu resource
    * bundle.
    * @param   key   the key.
    * @param   arguments   the arguments to format the string.
    * @return   the value.
    */
   private static String getLocalizedString(String key, Object... arguments) {
      String name = WoWAPI.class.getName();
      ResourceBundle bundle = ResourceBundle.getBundle(name, CharsetPropertiesControl.UTF_8_CONTROL);
      String pattern = bundle.getString(key);
      String message = MessageFormat.format(pattern, arguments);
      return message;
      }

   }
